//---------------------------------------------------------------------
//  This file is part of the Background Motion solution.
// 
//  Copyright (C) Mindscape (TM).  All rights reserved.
//  http://www.mindscape.co.nz
// 
//  THIS CODE AND INFORMATION ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY
//  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//  PARTICULAR PURPOSE.
//---------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.ComponentModel;

using Microsoft.Practices.EnterpriseLibrary.Validation;
using Microsoft.Practices.EnterpriseLibrary.Validation.Validators;

using Mindscape.BackgroundMotion.Core.Model;
using Mindscape.BackgroundMotion.Core.Utilities;
using Mindscape.BackgroundMotion.Model.Properties;
using System.Data.Linq;

namespace Mindscape.BackgroundMotion.Model
{
  /// <summary>
  /// Represents the base class for all model objects in the system
  /// </summary>
  /// <remarks>
  /// Indicates self validation for the Enterprise Library Validation framework
  /// </remarks>
  [HasSelfValidation]
  public abstract class ModelObject : IValidatable, ISelfValidating,
    INotifyPropertyChanged, IIdentifiable
  {
    public event PropertyChangedEventHandler PropertyChanging;
    public event PropertyChangedEventHandler PropertyChanged;

    private ValidationResults _instanceValidationResults;
    private readonly Set<ModelObject> _invalidChildren = new Set<ModelObject>();

    public abstract int Id { get; set; }

    protected ModelObject()
    {
      Validate();
    }

    /// <summary>
    /// Validates a required field
    /// </summary>
    /// <param name="fieldName">The name of the field being validated</param>
    /// <param name="targetId">The target unique identity of the object being validated</param>
    /// <param name="targetProperty">The target property of the object being validated</param>
    /// <param name="results">The validation results to add any output from validation to</param>
    protected static void Require(string fieldName, int targetId, object targetProperty, ValidationResults results)
    {
      ValidationResults idValidationResult = new ValidationResults();

      new Int32RangeValidator(0, RangeBoundaryType.Exclusive, Int32.MaxValue, RangeBoundaryType.Inclusive,
        string.Format(Resources.ValueIsRequired, fieldName)).Validate(targetId, idValidationResult);

      ValidationResults propertyValidationResults = new ValidationResults();

      new NotNullValidator(string.Format(Resources.ValueIsRequired, fieldName)).Validate(targetProperty, propertyValidationResults);

      if (!idValidationResult.IsValid && !propertyValidationResults.IsValid)
      {
        results.AddAllResults(propertyValidationResults);
      }
    }

    /// <summary>
    /// Called when a property on the model is changing.
    /// </summary>
    /// <param name="propertyName">Name of the property.</param>
    protected virtual void OnPropertyChanging(string propertyName)
    {
      if (PropertyChanging != null)
      {
        PropertyChanging(this, new PropertyChangedEventArgs(propertyName));
      }
    }

    /// <summary>
    /// Called when a property on the model has changed.
    /// </summary>
    /// <param name="propertyName">Name of the property.</param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
      Validate();

      if (PropertyChanged != null)
      {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
      }
    }

    #region Association Management

    protected delegate void BackReferenceWirer<T>(T t);

    /// <summary>
    /// Creates an entity set associated with the current model object
    /// </summary>
    protected EntitySet<T> CreateEntitySet<T>(BackReferenceWirer<T> unwire, BackReferenceWirer<T> wireUp)
      where T : ModelObject, new()
    {
      Action<T> attach = delegate(T t)
        {
          OnPropertyChanging(null);
          wireUp(t);
          OnPropertyChanged(null);
          AssociatedObjectChanged(t, new PropertyChangedEventArgs(null));
          t.PropertyChanged += AssociatedObjectChanged;
        };

      Action<T> detach = delegate(T t)
        {
          OnPropertyChanging(null);
          unwire(t);
          OnPropertyChanged(null);
          _invalidChildren.Remove(t);
          t.PropertyChanged -= AssociatedObjectChanged;

          using (UnitOfWork.Begin())
          {
            Repository<T>.Remove(t);
          }
        };

      EntitySet<T> entitySet = new EntitySet<T>
        (
        new Action<T>(attach),
        new Action<T>(detach)
        );

      return entitySet;
    }

    /// <summary>
    /// Updates the association.
    /// </summary>
    /// <param name="propertyName">Name of the property.</param>
    /// <param name="entityRef">The entity ref.</param>
    /// <param name="newValue">The new value.</param>
    /// <param name="unwire">The unwire.</param>
    /// <param name="wireUp">The wire up.</param>
    protected void UpdateAssociation<T>(string propertyName, ref EntityRef<T> entityRef, T newValue,
      BackReferenceWirer<T> unwire, BackReferenceWirer<T> wireUp)
      where T : ModelObject, new()
    {
      T oldValue = entityRef.Entity;

      if (oldValue != newValue)
      {
        OnPropertyChanging(propertyName);

        if (oldValue != null)
        {
          entityRef.Entity = null;
          unwire(oldValue);
          _invalidChildren.Remove(oldValue);
          oldValue.PropertyChanged -= AssociatedObjectChanged;
        }

        entityRef.Entity = newValue;

        if (newValue != null)
        {
          wireUp(newValue);
          AssociatedObjectChanged(newValue, new PropertyChangedEventArgs(null));
          newValue.PropertyChanged += AssociatedObjectChanged;
        }

        OnPropertyChanged(propertyName);
      }
    }

    /// <summary>
    /// Fires when an object associated with this model object has changed
    /// </summary>
    /// <remarks>
    /// We maintain a list of "valid" and "invalid" children which are assessed based on their current validation state
    /// This list is updated based on the state change triggering this call
    /// </remarks>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
    protected void AssociatedObjectChanged(object sender, PropertyChangedEventArgs e)
    {
      ModelObject modelObject = (ModelObject)sender;

      if (!modelObject.ValidationResults.IsValid)
      {
        _invalidChildren.Add(modelObject);
      }
      else
      {
        _invalidChildren.Remove(modelObject);
      }
    }

    #endregion

    /// <summary>
    /// Returns a set of ValidationResults for the current object
    /// </summary>
    /// <remarks>
    /// The object and all of its children are walked for their results at the same time through the CollectValidationResults method 
    /// </remarks>
    public ValidationResults ValidationResults
    {
      get
      {
        ValidationResults results = new ValidationResults();
        List<ModelObject> visited = new List<ModelObject>();

        CollectValidationResults(results, visited);

        return results;
      }
    }

    /// <summary>
    /// Performs a validation against marked up attributes
    /// </summary>
    /// <remarks>
    /// This method is called on any property change and updates the set of validation results
    /// The set of validation results is then used when we want to assess validation
    /// </remarks>
    private void Validate()
    {
      _instanceValidationResults = Validation.ValidateFromAttributes(this);
    }

    /// <summary>
    /// Implements self validation by calling Validate()
    /// </summary>
    /// <param name="results"></param>
    [SelfValidation]
    void ISelfValidating.Validate(ValidationResults results)
    {
      Validate(results);
    }

    /// <summary>
    /// Abstract method which Model Objects will implement to perform self validation
    /// </summary>
    /// <param name="results"></param>
    protected abstract void Validate(ValidationResults results);

    /// <summary>
    /// Used to collate validation results from objects in the current object graph while avoiding circular references
    /// </summary>
    /// <param name="results">The set of Validation Results to add error messages to</param>
    /// <param name="visited">The set of objects that have been visited so far in the trip</param>
    internal void CollectValidationResults(ValidationResults results, IList<ModelObject> visited)
    {
      visited.Add(this);
      results.AddAllResults(_instanceValidationResults);

      foreach (ModelObject invalidChild in _invalidChildren)
      {
        if (!visited.Contains(invalidChild))
        {
          invalidChild.CollectValidationResults(results, visited);
        }
      }
    }
  }
}